'use strict'

/**
 * Unqiue model object.
 *
 * @private
 * @type {Object}
 */
let um = {
    model: require('../model')
};

/**
 * The base class of all types.
 *
 * @public
 */
class BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {String} [name] - The name of type.
     */
    constructor(name) {
        this._name = name;
    }

    /**
     * Get the name of type.
     *
     * @public
     * @type {String}
     */
    get name() {
        return this._name;
    }

    /**
     * Set the name of type.
     *
     * @public
     * @type {String}
     */
    set name(name) {
        this._name = name;
    }

    /**
     * Convert the JavaScript value to the standard type of the model type.
     * It will call _fromValue to complete the task.
     *
     * @public
     *
     * @param {?*} [value] - The value of data
     *
     * @return {*} The value which is the standard type of model type. Return null if value is null.
     *
     * @example 
     *  let data = {
     *      name: 'test',
     *      password: '123'
     *  }; 
     *
     *  let model = type.fromValue(data);
     */
    fromValue(value) {
        if ( value === null ) {
            return null;
        }
        if ( value === undefined ) {
            return null;
        }

        return this._fromValue(value);
    }

    toValue(value) {
        if ( value === null ) {
            return null;
        }
        if ( value === undefined ) {
            return null;
        }

        return this._toValue(value);
    }
    
    /**
     * Convert the JavaScript value to the standard type of the model type.
     * It is called by fromValue.
     * You muse implement the method in your own type.
     *
     * @protected
     * @abstract
     *
     * @param {!*} [value] - The value of data
     *
     * @return {Any type} The value which is the standard type of model type.
     */
    _fromValue(value) {
        return null;
    }

    _toValue(value) {
        return null;
    }
}

/**
 * The field type of integer.
 * It use Number to represent the value.
 *
 * @public
 */
class Integer extends BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {Object} [options] - The options of type.
     */
    constructor(options) {
        super('Integer');
    }

    /**
     * Convert the value to integer.
     * The type of value is Number.
     * If the value is decimal fraction, it will truncate it into integer.
     *
     * @protected
     * @override
     *
     * @param {!*} [value] - The value of data
     *
     * @return {Number} The integer value
     */
    _fromValue(value) {
        return Math.trunc(Number(value));
    }

    _toValue(value) {
        return value;
    }
}

/**
 * The field type of double.
 * It use Number to represent the value.
 *
 * @public
 */
class Double extends BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {Object} [options] - The options of type.
     */
    constructor(options) {
        super('Double');
    }

    /**
     * Convert the value to double.
     * The type of value is Number.
     *
     * @protected
     * @override
     *
     * @param {!*} [value] - The value of data
     *
     * @return {Number} The double value
     */
    _fromValue(value) {
        return Number(value);
    }

    _toValue(value) {
        return value;
    }
}

/**
 * The field type of boolean.
 * It use Boolean to represent the value.
 *
 * @public
 */
class Bool extends BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {Object} [options] - The options of type.
     */
    constructor(options) {
        super('Bool');
    }

    /**
     * Convert the value to double.
     * The type of value is Number.
     *
     * @protected
     * @override
     *
     * @param {!*} [value] - The value of data
     *
     * @return {Number} The double value
     */
    _fromValue(value) {
        return Boolean(value);
    }

    _toValue(value) {
        return value;
    }
}

/**
 * The field type of text.
 * It use String to represent the value.
 *
 * @public
 */
class Text extends BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {Object} [options] - The options of type.
     */
    constructor(options) {
        super('Text');
    }

    /**
     * Convert the value to text.
     * The type of value is String.
     *
     * @protected
     * @override
     *
     * @param {!*} [value] - The value of data
     *
     * @return {String} The string value
     */
    _fromValue(value) {
        return String(value);
    }

    _toValue(value) {
        return value;
    }
}

/**
 * The field type of date.
 * It use Date to represent the value.
 *
 * @public
 */
class DateTime extends BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {Object} [options] - The options of type.
     */
    constructor(options) {
        super('DateTime');
    }

    /**
     * Convert the value to double.
     * The type of value is Number.
     *
     * @protected
     * @override
     *
     * @param {!*} [value] - The value of data
     *
     * @return {Number} The double value
     */
    _fromValue(value) {
        return new Date(value);
    }

    _toValue(value) {
        return value;
    }
}

/**
 * The field type of unique object.
 * It use UniqueModel to represent the value.
 *
 * @public
 */
class UObject extends BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {Object} [options] - The options of type.
     * @param {String} [options.type] - The type model name of object.
     */
    constructor(options) {
        super('UObject');

        if ( 'type' in options) {
            /**
             * The type of object(the unique model name).
             *
             * @type {String}
             */
            this.type = options.type;
        }
        else {
            throw 'You must specify the type of object';
        }
    }

    /**
     * Convert the value to unique object.
     * The type of value is UniqueModel.
     *
     * @protected
     * @override
     *
     * @param {!Object} [value] - The value of data.
     *
     * @return {model/base/Model} The UniqueModel object coresponding to value.
     */
    _fromValue(value) {
        let model = um.model.getModel(this.type);
        if ( value instanceof model ) {
            return value;
        }

        let uniqueModel = toUniqueModel(model, value);
        uniqueModel.id = value.id;

        return uniqueModel;
    }

    _toValue(value) {
        return {
            id: value.id
        };
    }
}

/**
 * The field type of array of unique object.
 * It use UniqueModel to represent the element of array.
 *
 * @public
 */
class UObjectArray extends BaseType {
    /**
     * The constructor of class.
     *
     * @public
     * @param {Object} [options] - The options of type.
     * @param {String} [options.type] - The type model name of object.
     */
    constructor(options) {
        super('UObjectArray');

        if ( 'type' in options) {
            /**
             * The type of object(the unique model name).
             *
             * @type {String}
             */
            this.type = options.type;
        }
        else {
            throw 'You must specify the type of object';
        }
    }

    /**
     * Convert the value to unique object.
     * The type of value is UniqueModel.
     *
     * @protected
     * @override
     *
     * @param {!Array<Object>} [array] - The array of object.
     *
     * @return {model/base/Model} The UniqueModel object coresponding to value.
     */
    _fromValue(array) {
        let model = um.model.getModel(this.type);
        
        let uniqueModels = array.map(value => {
            if ( value instanceof model ) {
                return value;
            }

            let uniqueModel = toUniqueModel(model, value);
            uniqueModel.id = value.id;

            return uniqueModel;
        }); 

        return uniqueModels;
    }

    _toValue(array) {
        return array.map(value => {
            return {
                id: value.id
            };
        });
    }
}

/*
 * @private
 */
let allTypes = {
    Integer,
    Double,
    Text,
    UObject,
    Bool,
    DateTime,
    UObjectArray
};

/**
 * Convert the JavaScript object to unique object.
 * If you pass a UniqueModel object, it will replace the attributes in the object.
 * Or the function will create a new UniqueModel object.
 *
 * @public
 *
 * @param {class<model/base/Model>} [uniqueModel] - The UniqueModel type.
 * @param {Object} [objectToCopy] - The JavaScript object.
 * @param {model/base/Model} [uniqueModelObject] - The UniqueModel object.
 *
 * @return {model/base/Model} The UniqueModel object.
 */
function toUniqueModel(uniqueModel, objectToCopy, uniqueModelObject) {
    let schema = uniqueModel._meta.schema;

    if ( !uniqueModelObject ) {
        uniqueModelObject = new uniqueModel();
    }

    for ( let fieldName in schema ) {
        let fieldType = schema[fieldName];

        let fieldValue = objectToCopy[fieldName];
        uniqueModelObject[fieldName] = fieldValue;
    }
    if ( objectToCopy._id ) {
        uniqueModelObject.id = objectToCopy._id;
    }
    else if ( objectToCopy.id ) {
        uniqueModelObject.id = objectToCopy.id;
    }

    return uniqueModelObject;
}

/**
 * Generate the function to define type.
 *
 * @public
 *
 * @param {String} [name] - The name of type.
 *
 * @return {function(name: String, options: Object): BaseType} The function to create type object.
 */
function defineType(name) {
    let executor = function(name, options) {
        let typeClass = allTypes[name];
        let type = new typeClass(options);

        return type;
    }

    return executor.bind(null, name);
}

module.exports = {
    Integer: defineType('Integer'),
    Double: defineType('Double'),
    Bool: defineType('Bool'),
    Text: defineType('Text'),
    UObject: defineType('UObject'),
    UObjectArray: defineType('UObjectArray'),
    DateTime: defineType('DateTime'),
    toUniqueModel
};
